;(function(exports, ByteArray, global) {
  var Protocol = exports

  var PKG_HEAD_BYTES = 4
  var MSG_FLAG_BYTES = 1
  var MSG_ROUTE_CODE_BYTES = 2
  var MSG_ID_MAX_BYTES = 5
  var MSG_ROUTE_LEN_BYTES = 1

  var MSG_ROUTE_CODE_MAX = 0xffff

  var MSG_COMPRESS_ROUTE_MASK = 0x1
  var MSG_TYPE_MASK = 0x7

  var Package = (Protocol.Package = {})
  var Message = (Protocol.Message = {})

  Package.TYPE_HANDSHAKE = 1
  Package.TYPE_HANDSHAKE_ACK = 2
  Package.TYPE_HEARTBEAT = 3
  Package.TYPE_DATA = 4
  Package.TYPE_KICK = 5

  Message.TYPE_REQUEST = 0
  Message.TYPE_NOTIFY = 1
  Message.TYPE_RESPONSE = 2
  Message.TYPE_PUSH = 3

  /**
   * pomele client encode
   * id message id;
   * route message route
   * msg message body
   * socketio current support string
   */
  Protocol.strencode = function(str) {
    var byteArray = new ByteArray(str.length * 3)
    var offset = 0
    for (var i = 0; i < str.length; i++) {
      var charCode = str.charCodeAt(i)
      var codes = null
      if (charCode <= 0x7f) {
        codes = [charCode]
      } else if (charCode <= 0x7ff) {
        codes = [0xc0 | (charCode >> 6), 0x80 | (charCode & 0x3f)]
      } else {
        codes = [
          0xe0 | (charCode >> 12),
          0x80 | ((charCode & 0xfc0) >> 6),
          0x80 | (charCode & 0x3f)
        ]
      }
      for (var j = 0; j < codes.length; j++) {
        byteArray[offset] = codes[j]
        ++offset
      }
    }
    var _buffer = new ByteArray(offset)
    copyArray(_buffer, 0, byteArray, 0, offset)
    return _buffer
  }

  /**
   * client decode
   * msg String data
   * return Message Object
   */
  Protocol.strdecode = function(buffer) {
    var bytes = new ByteArray(buffer)
    var array = []
    var offset = 0
    var charCode = 0
    var end = bytes.length
    while (offset < end) {
      if (bytes[offset] < 128) {
        charCode = bytes[offset]
        offset += 1
      } else if (bytes[offset] < 224) {
        charCode = ((bytes[offset] & 0x3f) << 6) + (bytes[offset + 1] & 0x3f)
        offset += 2
      } else {
        charCode =
          ((bytes[offset] & 0x0f) << 12) +
          ((bytes[offset + 1] & 0x3f) << 6) +
          (bytes[offset + 2] & 0x3f)
        offset += 3
      }
      array.push(charCode)
    }
    return String.fromCharCode.apply(null, array)
  }

  /**
   * Package protocol encode.
   *
   * Pomelo package format:
   * +------+-------------+------------------+
   * | type | body length |       body       |
   * +------+-------------+------------------+
   *
   * Head: 4bytes
   *   0: package type,
   *      1 - handshake,
   *      2 - handshake ack,
   *      3 - heartbeat,
   *      4 - data
   *      5 - kick
   *   1 - 3: big-endian body length
   * Body: body length bytes
   *
   * @param  {Number}    type   package type
   * @param  {ByteArray} body   body content in bytes
   * @return {ByteArray}        new byte array that contains encode result
   */
  Package.encode = function(type, body) {
    var length = body ? body.length : 0
    var buffer = new ByteArray(PKG_HEAD_BYTES + length)
    var index = 0
    buffer[index++] = type & 0xff
    buffer[index++] = (length >> 16) & 0xff
    buffer[index++] = (length >> 8) & 0xff
    buffer[index++] = length & 0xff
    if (body) {
      copyArray(buffer, index, body, 0, length)
    }
    return buffer
  }

  /**
   * Package protocol decode.
   * See encode for package format.
   *
   * @param  {ByteArray} buffer byte array containing package content
   * @return {Object}           {type: package type, buffer: body byte array}
   */
  Package.decode = function(buffer) {
    var offset = 0
    var bytes = new ByteArray(buffer)
    var length = 0
    var rs = []
    while (offset < bytes.length) {
      var type = bytes[offset++]
      length =
        ((bytes[offset++] << 16) | (bytes[offset++] << 8) | bytes[offset++]) >>>
        0
      var body = length ? new ByteArray(length) : null
      copyArray(body, 0, bytes, offset, length)
      offset += length
      rs.push({ type: type, body: body })
    }
    return rs.length === 1 ? rs[0] : rs
  }

  /**
   * Message protocol encode.
   *
   * @param  {Number} id            message id
   * @param  {Number} type          message type
   * @param  {Number} compressRoute whether compress route
   * @param  {Number|String} route  route code or route string
   * @param  {Buffer} msg           message body bytes
   * @return {Buffer}               encode result
   */
  Message.encode = function(id, type, compressRoute, route, msg) {
    // caculate message max length
    var idBytes = msgHasId(type) ? caculateMsgIdBytes(id) : 0
    var msgLen = MSG_FLAG_BYTES + idBytes

    if (msgHasRoute(type)) {
      if (compressRoute) {
        if (typeof route !== 'number') {
          throw new Error('error flag for number route!')
        }
        msgLen += MSG_ROUTE_CODE_BYTES
      } else {
        msgLen += MSG_ROUTE_LEN_BYTES
        if (route) {
          route = Protocol.strencode(route)
          if (route.length > 255) {
            throw new Error('route maxlength is overflow')
          }
          msgLen += route.length
        }
      }
    }

    if (msg) {
      msgLen += msg.length
    }

    var buffer = new ByteArray(msgLen)
    var offset = 0

    // add flag
    offset = encodeMsgFlag(type, compressRoute, buffer, offset)

    // add message id
    if (msgHasId(type)) {
      offset = encodeMsgId(id, buffer, offset)
    }

    // add route
    if (msgHasRoute(type)) {
      offset = encodeMsgRoute(compressRoute, route, buffer, offset)
    }

    // add body
    if (msg) {
      offset = encodeMsgBody(msg, buffer, offset)
    }

    return buffer
  }

  /**
   * Message protocol decode.
   *
   * @param  {Buffer|Uint8Array} buffer message bytes
   * @return {Object}            message object
   */
  Message.decode = function(buffer) {
    var bytes = new ByteArray(buffer)
    var bytesLen = bytes.length || bytes.byteLength
    var offset = 0
    var id = 0
    var route = null

    // parse flag
    var flag = bytes[offset++]
    var compressRoute = flag & MSG_COMPRESS_ROUTE_MASK
    var type = (flag >> 1) & MSG_TYPE_MASK

    // parse id
    if (msgHasId(type)) {
      var m = parseInt(bytes[offset])
      var i = 0
      do {
        var m = parseInt(bytes[offset])
        id = id + (m & 0x7f) * Math.pow(2, 7 * i)
        offset++
        i++
      } while (m >= 128)
    }

    // parse route
    if (msgHasRoute(type)) {
      if (compressRoute) {
        route = (bytes[offset++] << 8) | bytes[offset++]
      } else {
        var routeLen = bytes[offset++]
        if (routeLen) {
          route = new ByteArray(routeLen)
          copyArray(route, 0, bytes, offset, routeLen)
          route = Protocol.strdecode(route)
        } else {
          route = ''
        }
        offset += routeLen
      }
    }

    // parse body
    var bodyLen = bytesLen - offset
    var body = new ByteArray(bodyLen)

    copyArray(body, 0, bytes, offset, bodyLen)

    return {
      id: id,
      type: type,
      compressRoute: compressRoute,
      route: route,
      body: body
    }
  }

  var copyArray = function(dest, doffset, src, soffset, length) {
    if ('function' === typeof src.copy) {
      // Buffer
      src.copy(dest, doffset, soffset, soffset + length)
    } else {
      // Uint8Array
      for (var index = 0; index < length; index++) {
        dest[doffset++] = src[soffset++]
      }
    }
  }

  var msgHasId = function(type) {
    return type === Message.TYPE_REQUEST || type === Message.TYPE_RESPONSE
  }

  var msgHasRoute = function(type) {
    return (
      type === Message.TYPE_REQUEST ||
      type === Message.TYPE_NOTIFY ||
      type === Message.TYPE_PUSH
    )
  }

  var caculateMsgIdBytes = function(id) {
    var len = 0
    do {
      len += 1
      id >>= 7
    } while (id > 0)
    return len
  }

  var encodeMsgFlag = function(type, compressRoute, buffer, offset) {
    if (
      type !== Message.TYPE_REQUEST &&
      type !== Message.TYPE_NOTIFY &&
      type !== Message.TYPE_RESPONSE &&
      type !== Message.TYPE_PUSH
    ) {
      throw new Error('unkonw message type: ' + type)
    }

    buffer[offset] = (type << 1) | (compressRoute ? 1 : 0)

    return offset + MSG_FLAG_BYTES
  }

  var encodeMsgId = function(id, buffer, offset) {
    do {
      var tmp = id % 128
      var next = Math.floor(id / 128)

      if (next !== 0) {
        tmp = tmp + 128
      }
      buffer[offset++] = tmp

      id = next
    } while (id !== 0)

    return offset
  }

  var encodeMsgRoute = function(compressRoute, route, buffer, offset) {
    if (compressRoute) {
      if (route > MSG_ROUTE_CODE_MAX) {
        throw new Error('route number is overflow')
      }

      buffer[offset++] = (route >> 8) & 0xff
      buffer[offset++] = route & 0xff
    } else {
      if (route) {
        buffer[offset++] = route.length & 0xff
        copyArray(buffer, offset, route, 0, route.length)
        offset += route.length
      } else {
        buffer[offset++] = 0
      }
    }

    return offset
  }

  var encodeMsgBody = function(msg, buffer, offset) {
    copyArray(buffer, offset, msg, 0, msg.length)
    return offset + msg.length
  }

  if (typeof window != 'undefined') {
    window.Protocol = Protocol
  }
})(
  typeof window == 'undefined' ? module.exports : (window.Protocol = {}),
  typeof window == 'undefined' ? Buffer : Uint8Array,
  window
)
